{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "eec680f5",
   "metadata": {},
   "source": [
    "# 사람에게 물어보는 노드\n",
    "\n",
    "지금까지는 메시지들의 상태(State) 에 의존해 왔습니다. \n",
    "\n",
    "이런 상태 값들의 수정으로도 많은 것을 할 수 있지만, 메시지 목록에만 의존하지 않고 복잡한 동작을 정의하고자 한다면 상태에 추가 필드를 더할 수 있습니다. 이 튜토리얼에서는 새로운 노드를 추가하여 챗봇을 확장하는 방법을 설명합니다.\n",
    "\n",
    "위의 예에서는 도구가 호출될 때마다 **interrupt 를 통해 그래프가 항상 중단** 되도록 Human-in-the-loop 을 구현하였습니다. \n",
    "\n",
    "이번에는, 챗봇이 인간에 의존할지 선택할 수 있도록 하고 싶다고 가정해 봅시다.\n",
    "\n",
    "이를 수행하는 한 가지 방법은 그래프가 항상 멈추는 **\"human\" 노드** 를 생성하는 것입니다. 이 노드는 LLM이 \"human\" 도구를 호출할 때만 실행됩니다. 편의를 위해, 그래프 상태에 \"ask_human\" 플래그를 포함시켜 LLM이 이 도구를 호출하면 플래그를 전환하도록 할 것입니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "de9d9d8d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6b5c6228",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH17-LangGraph-Modules\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f71c3748",
   "metadata": {},
   "source": [
    "## 사람에게 의견을 묻는 노드 설정"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "fd16b08c",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Annotated\n",
    "from typing_extensions import TypedDict\n",
    "from langchain_teddynote.tools.tavily import TavilySearch\n",
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "from langgraph.graph import StateGraph, START\n",
    "from langgraph.graph.message import add_messages\n",
    "from langgraph.prebuilt import ToolNode, tools_condition"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d1c75470",
   "metadata": {},
   "source": [
    "이번에는 중간에 사람에게 질문할지 여부를 묻는 상태(`ask_human`)를 추가합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "1a559e3f",
   "metadata": {},
   "outputs": [],
   "source": [
    "class State(TypedDict):\n",
    "    # 메시지 목록\n",
    "    messages: Annotated[list, add_messages]\n",
    "    # 사람에게 질문할지 여부를 묻는 상태 추가\n",
    "    ask_human: bool"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "916ab204",
   "metadata": {},
   "source": [
    "`human` 에 대한 요청시 사용되는 스키마를 정의합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "3d863e2e",
   "metadata": {},
   "outputs": [],
   "source": [
    "from pydantic import BaseModel\n",
    "\n",
    "\n",
    "class HumanRequest(BaseModel):\n",
    "    \"\"\"Forward the conversation to an expert. Use when you can't assist directly or the user needs assistance that exceeds your authority.\n",
    "    To use this function, pass the user's 'request' so that an expert can provide appropriate guidance.\n",
    "    \"\"\"\n",
    "\n",
    "    request: str"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "95849624",
   "metadata": {},
   "source": [
    "다음으로, 챗봇 노드를 정의합니다. \n",
    "\n",
    "여기서 주요 수정 사항은 챗봇이 `RequestAssistance` 플래그를 호출한 경우 `ask_human` 플래그를 전환하는 것입니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "8873be22",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# 도구 추가\n",
    "tool = TavilySearch(max_results=3)\n",
    "\n",
    "# 도구 목록 추가(HumanRequest 도구)\n",
    "tools = [tool, HumanRequest]\n",
    "\n",
    "# LLM 추가\n",
    "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
    "\n",
    "# 도구 바인딩\n",
    "llm_with_tools = llm.bind_tools(tools)\n",
    "\n",
    "\n",
    "def chatbot(state: State):\n",
    "    # LLM 도구 호출을 통한 응답 생성\n",
    "    response = llm_with_tools.invoke(state[\"messages\"])\n",
    "\n",
    "    # 사람에게 질문할지 여부 초기화\n",
    "    ask_human = False\n",
    "\n",
    "    # 도구 호출이 있고 이름이 'HumanRequest' 인 경우\n",
    "    if response.tool_calls and response.tool_calls[0][\"name\"] == HumanRequest.__name__:\n",
    "        ask_human = True\n",
    "\n",
    "    # 메시지와 ask_human 상태 반환\n",
    "    return {\"messages\": [response], \"ask_human\": ask_human}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4aba6c07",
   "metadata": {},
   "source": [
    "다음으로, 그래프 빌더를 생성하고 이전과 동일하게 `chatbot` 과 `tools` 노드를 그래프에 추가합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "98c2f651",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 상태 그래프 초기화\n",
    "graph_builder = StateGraph(State)\n",
    "\n",
    "# 챗봇 노드 추가\n",
    "graph_builder.add_node(\"chatbot\", chatbot)\n",
    "\n",
    "# 도구 노드 추가\n",
    "graph_builder.add_node(\"tools\", ToolNode(tools=[tool]))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e7f3a9ac",
   "metadata": {},
   "source": [
    "## Human 노드 설정"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c36e9613",
   "metadata": {},
   "source": [
    "다음으로 `human` 노드를 생성합니다. \n",
    "\n",
    "이 노드는 주로 그래프에서 인터럽트를 트리거하는 자리 표시자 역할을 합니다. 사용자가 `interrupt` 동안 수동으로 상태를 업데이트하지 않으면, LLM이 사용자가 요청을 받았지만 응답하지 않았음을 알 수 있도록 도구 메시지를 삽입합니다. \n",
    "\n",
    "이 노드는 또한 `ask_human` 플래그를 해제하여 추가 요청이 없는 한 그래프가 노드를 다시 방문하지 않도록 합니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "612ad3f4",
   "metadata": {},
   "source": [
    "> 참고 이미지\n",
    "\n",
    "![](image/langgraph-07.jpeg)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "be22a936",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.messages import AIMessage, ToolMessage\n",
    "\n",
    "\n",
    "# 응답 메시지 생성(ToolMessage 생성을 위한 함수)\n",
    "def create_response(response: str, ai_message: AIMessage):\n",
    "    return ToolMessage(\n",
    "        content=response,\n",
    "        tool_call_id=ai_message.tool_calls[0][\"id\"],\n",
    "    )\n",
    "\n",
    "\n",
    "# 인간 노드 처리\n",
    "def human_node(state: State):\n",
    "    new_messages = []\n",
    "    if not isinstance(state[\"messages\"][-1], ToolMessage):\n",
    "        # 사람으로부터 응답이 없는 경우\n",
    "        new_messages.append(\n",
    "            create_response(\"No response from human.\", state[\"messages\"][-1])\n",
    "        )\n",
    "    return {\n",
    "        # 새 메시지 추가\n",
    "        \"messages\": new_messages,\n",
    "        # 플래그 해제\n",
    "        \"ask_human\": False,\n",
    "    }\n",
    "\n",
    "\n",
    "# 그래프에 인간 노드 추가\n",
    "graph_builder.add_node(\"human\", human_node)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "84be5ddc",
   "metadata": {},
   "source": [
    "다음으로, 조건부 논리를 정의합니다. \n",
    "\n",
    "`select_next_node`는 플래그가 설정된 경우 `human` 노드로 경로를 지정합니다. 그렇지 않으면, 사전 구축된 `tools_condition` 함수가 다음 노드를 선택하도록 합니다.\n",
    "\n",
    "`tools_condition` 함수는 단순히 `chatbot`이 응답 메시지에서 `tool_calls`을 사용했는지 확인합니다. \n",
    "\n",
    "사용한 경우, `action` 노드로 경로를 지정합니다. 그렇지 않으면, 그래프를 종료합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8baad803",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.graph import END\n",
    "\n",
    "\n",
    "# 다음 노드 선택\n",
    "def select_next_node(state: State):\n",
    "    # 인간에게 질문 여부 확인\n",
    "    if state[\"ask_human\"]:\n",
    "        return \"human\"\n",
    "    # 이전과 동일한 경로 설정\n",
    "    return tools_condition(state)\n",
    "\n",
    "\n",
    "# 조건부 엣지 추가\n",
    "graph_builder.add_conditional_edges(\n",
    "    \"chatbot\",\n",
    "    select_next_node,\n",
    "    {\"human\": \"human\", \"tools\": \"tools\", END: END},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a62cada4",
   "metadata": {},
   "source": [
    "마지막으로 엣지를 연결하고 그래프를 컴파일합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "119adb9d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 엣지 추가: 'tools'에서 'chatbot'으로\n",
    "graph_builder.add_edge(\"tools\", \"chatbot\")\n",
    "\n",
    "# 엣지 추가: 'human'에서 'chatbot'으로\n",
    "graph_builder.add_edge(\"human\", \"chatbot\")\n",
    "\n",
    "# 엣지 추가: START에서 'chatbot'으로\n",
    "graph_builder.add_edge(START, \"chatbot\")\n",
    "\n",
    "# 메모리 저장소 초기화\n",
    "memory = MemorySaver()\n",
    "\n",
    "# 그래프 컴파일: 메모리 체크포인터 사용\n",
    "graph = graph_builder.compile(\n",
    "    checkpointer=memory,\n",
    "    # 'human' 이전에 인터럽트 설정\n",
    "    interrupt_before=[\"human\"],\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "15e8347b",
   "metadata": {},
   "source": [
    "그래프를 시각화 합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bd4b6f0d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.graphs import visualize_graph\n",
    "\n",
    "visualize_graph(graph)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "90d371bb",
   "metadata": {},
   "source": [
    "`chatbot` 노드는 다음과 같은 동작을 합니다.\n",
    "\n",
    "- 챗봇은 인간에게 도움을 요청할 수 있으며 (chatbot->select->human)\n",
    "- 검색 엔진 도구를 호출하거나 (chatbot->select->action)\n",
    "- 직접 응답할 수 있습니다 (chatbot->select->__end__). \n",
    "\n",
    "일단 행동이나 요청이 이루어지면, 그래프는 `chatbot` 노드로 다시 전환되어 작업을 계속합니다. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ea50edc3",
   "metadata": {},
   "outputs": [],
   "source": [
    "# user_input = \"이 AI 에이전트를 구축하기 위해 전문가의 도움이 필요합니다. 검색해서 답변하세요\" (Human 이 아닌 웹검색을 수행하는 경우)\n",
    "user_input = \"이 AI 에이전트를 구축하기 위해 전문가의 도움이 필요합니다. 도움을 요청할 수 있나요?\"\n",
    "\n",
    "# config 설정\n",
    "config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
    "\n",
    "# 스트림 또는 호출의 두 번째 위치 인수로서의 구성\n",
    "events = graph.stream(\n",
    "    {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n",
    ")\n",
    "for event in events:\n",
    "    if \"messages\" in event:\n",
    "        # 마지막 메시지의 예쁜 출력\n",
    "        event[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2de5e6a4",
   "metadata": {},
   "source": [
    "**Notice:** `LLM`이 제공된 \"`HumanRequest`\" 도구를 호출했으며, 인터럽트가 설정되었습니다. 그래프 상태를 확인해 봅시다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f6613c99",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 그래프 상태 스냅샷 생성\n",
    "snapshot = graph.get_state(config)\n",
    "\n",
    "# 다음 스냅샷 상태 접근\n",
    "snapshot.next"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6951bb36",
   "metadata": {},
   "source": [
    "그래프 상태는 실제로 `'human'` 노드 이전에 **중단**됩니다. \n",
    "\n",
    "이 시나리오에서 \"전문가\"로서 행동하고 입력을 사용하여 새로운 `ToolMessage`를 추가하여 상태를 수동으로 업데이트할 수 있습니다.\n",
    "\n",
    "다음으로, 챗봇의 요청에 응답하려면 다음을 수행합니다.\n",
    "\n",
    "1. 응답을 포함한 `ToolMessage`를 생성합니다. 이는 `chatbot`에 다시 전달됩니다.\n",
    "2. `update_state`를 호출하여 그래프 상태를 수동으로 업데이트합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0be4518f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# AI 메시지 추출\n",
    "ai_message = snapshot.values[\"messages\"][-1]\n",
    "\n",
    "# 인간 응답 생성\n",
    "human_response = (\n",
    "    \"전문가들이 도와드리겠습니다! 에이전트 구축을 위해 LangGraph를 확인해 보시기를 적극 추천드립니다. \"\n",
    "    \"단순한 자율 에이전트보다 훨씬 더 안정적이고 확장성이 뛰어납니다. \"\n",
    "    \"https://wikidocs.net/233785 에서 더 많은 정보를 확인할 수 있습니다.\"\n",
    ")\n",
    "\n",
    "# 도구 메시지 생성\n",
    "tool_message = create_response(human_response, ai_message)\n",
    "\n",
    "# 그래프 상태 업데이트\n",
    "graph.update_state(config, {\"messages\": [tool_message]})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b7880e9e",
   "metadata": {},
   "source": [
    "상태를 확인하여 응답이 추가되었는지 확인할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b0ff4e47",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 그래프 상태에서 메시지 값 가져오기\n",
    "graph.get_state(config).values[\"messages\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "718d2f16",
   "metadata": {},
   "source": [
    "다음으로, 입력값으로 `None`을 사용하여 그래프를 **resume**합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "077595dc",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 그래프에서 이벤트 스트림 생성\n",
    "events = graph.stream(None, config, stream_mode=\"values\")\n",
    "\n",
    "# 각 이벤트에 대한 처리\n",
    "for event in events:\n",
    "    # 메시지가 있는 경우 마지막 메시지 출력\n",
    "    if \"messages\" in event:\n",
    "        event[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ed7ac070",
   "metadata": {},
   "source": [
    "최종 결과를 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0dd70bfe",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 최종 상태 확인\n",
    "state = graph.get_state(config)\n",
    "\n",
    "# 단계별 메시지 출력\n",
    "for message in state.values[\"messages\"]:\n",
    "    message.pretty_print()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-kr-lwwSZlnu-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
